Skip to content

NestJS + socket.io 开发聊天室

第一步:安装依赖

shell
pnpm add @nestjs/platform-socket.io @nestjs/platform-ws @nestjs/websockets socket.io
pnpm add @types/socket.io -D

第二步:核心逻辑

src/chat/chat.gateway.ts

js
// chat.gateway.ts
import {
  WebSocketGateway,
  WebSocketServer,
  SubscribeMessage,
  OnGatewayConnection,
  OnGatewayDisconnect,
  ConnectedSocket,
  MessageBody,
} from '@nestjs/websockets';
import { Server, Socket } from 'socket.io';

// 定义用户接口
interface User {
  id: string;
  username: string;
  socketId: string;
}

@WebSocketGateway({
  cors: {
    origin: '*',
    credentials: true,
  },
  namespace: 'chat',
})
export class ChatGateway implements OnGatewayConnection, OnGatewayDisconnect {
  @WebSocketServer()
  server: Server;

  // 添加 users 属性
  private users: Map<string, User> = new Map();

  // 监听客户端连接
  handleConnection(client: Socket) {
    console.log('\n========== 新客户端连接 ==========');
    console.log('客户端 Socket ID:', client.id);
    console.log('客户端 IP:', client.handshake.address);
    console.log('客户端 Headers:', client.handshake.headers);
    console.log('客户端 Query 参数:', client.handshake.query);
    console.log('传输方式:', client.conn.transport);
    console.log('===================================\n');
  }

  // 监听客户端断开连接
  handleDisconnect(client: Socket) {
    console.log('\n========== 客户端断开连接 ==========');
    console.log('客户端 Socket ID:', client.id);
    console.log('断开时间:', new Date().toLocaleString());
    console.log('===================================\n');

    // 移除断开连接的用户
    const user = Array.from(this.users.values()).find(
      (u) => u.socketId === client.id,
    );
    if (user) {
      this.users.delete(user.id);
      // 广播用户离开消息
      this.server.emit('usersUpdate', Array.from(this.users.values()));
    }
  }

  // 用户登录 - 打印详细客户端信息
  @SubscribeMessage('login')
  handleLogin(
    @ConnectedSocket() client: Socket,
    @MessageBody() data: { username: string },
  ) {
    console.log('\n========== 用户登录 ==========');
    console.log('客户端信息:');
    console.log('  - Socket ID:', client.id);
    console.log('  - IP地址:', client.handshake.address);
    console.log('  - 连接的房间:', this.getClientRooms(client));
    console.log('  - 是否已认证:', client.handshake.auth);
    console.log('用户数据:');
    console.log('  - 用户名:', data.username);
    console.log('  - 完整数据:', JSON.stringify(data, null, 2));
    console.log('===============================\n');

    // 可以打印更多客户端信息
    this.printClientDetails(client);

    // 保存用户信息
    const user: User = {
      id: Date.now().toString(),
      username: data.username,
      socketId: client.id,
    };
    this.users.set(user.id, user);

    // 广播用户加入
    this.server.emit('usersUpdate', Array.from(this.users.values()));

    // 业务逻辑...
    return { success: true, user: { id: client.id, username: data.username } };
  }

  // 发送消息 - 打印消息内容和客户端信息
  @SubscribeMessage('message')
  async handleMessage(
    @ConnectedSocket() client: Socket,
    @MessageBody() data: { username: string; content: string },
  ) {
    const realOnlineCount = await this.getOnlineUsersCount();
    console.log('\n========== 收到新消息 ==========');
    console.log('发送者:');
    console.log('  - Socket ID:', client.id);
    console.log('  - 用户名:', data.username);
    console.log('消息内容:');
    console.log('  - 文本:', data.content);
    console.log('  - 长度:', data.content.length);
    console.log('  - 时间:', new Date().toLocaleString());
    console.log('在线用户数:', realOnlineCount);
    console.log('================================\n');

    // 广播消息...
    this.server.emit('message', {
      id: Date.now().toString(),
      username: data.username,
      content: data.content,
      timestamp: new Date(),
    });
  }

  // 私聊消息 - 打印详细信息
  @SubscribeMessage('privateMessage')
  handlePrivateMessage(
    @ConnectedSocket() client: Socket,
    @MessageBody()
    data: { toUserId: string; fromUsername: string; content: string },
  ) {
    console.log('\n========== 私聊消息 ==========');
    console.log('发送方信息:');
    console.log('  - Socket ID:', client.id);
    console.log('  - 用户名:', data.fromUsername);
    console.log('接收方信息:');
    console.log('  - 用户ID:', data.toUserId);
    console.log('消息内容:');
    console.log('  - 文本:', data.content);
    console.log('===============================\n');

    // 查找目标用户
    const targetUser = this.users.get(data.toUserId);

    if (targetUser) {
      const privateMessage = {
        id: Date.now().toString(),
        username: data.fromUsername,
        content: `[私聊] ${data.content}`,
        timestamp: new Date(),
        type: 'private',
      };

      // 发送给目标用户
      this.server
        .to(targetUser.socketId)
        .emit('privateMessage', privateMessage);
      // 发送给发送方确认
      client.emit('privateMessage', {
        ...privateMessage,
        content: `[私发给 ${targetUser.username}] ${data.content}`,
      });
    }
  }

  // 获取在线用户 - 打印请求信息
  @SubscribeMessage('getUsers')
  handleGetUsers(@ConnectedSocket() client: Socket) {
    console.log(
      `\n[${new Date().toLocaleTimeString()}] 客户端 ${client.id} 请求在线用户列表`,
    );
    client.emit('usersUpdate', Array.from(this.users.values()));
  }

  // 辅助方法:获取客户端房间列表(类型安全)
  private getClientRooms(client: Socket): string[] {
    // 方法1:使用 Set 迭代器
    const rooms: string[] = [];
    if (client.rooms) {
      // 将 Set 转换为数组
      if (typeof client.rooms.forEach === 'function') {
        client.rooms.forEach((room: string) => {
          rooms.push(room);
        });
      }
      // 或者使用扩展运算符(需要配置 tsconfig)
      // return [...client.rooms];
    }
    return rooms;
  }

  // 辅助方法:获取在线用户数量
  private async getOnlineUsersCount(): Promise<number> {
    try {
      // 方法1:获取所有连接的 sockets
      const sockets = await this.server.fetchSockets();
      return sockets.length;
    } catch (error) {
      console.error('获取在线用户数失败:', error);
      return 0;
    }
  }

  // 辅助方法:打印客户端详细信息(修复 rooms 类型)
  private printClientDetails(client: Socket) {
    console.log('\n========== 客户端详细信息 ==========');
    console.log('1. 基础信息:');
    console.log(`   - ID: ${client.id}`);
    console.log(`   - 是否连接: ${client.connected}`);
    console.log(`   - 是否断开: ${client.disconnected}`);

    console.log('\n2. 握手信息:');
    console.log(`   - 地址: ${client.handshake.address}`);
    console.log(`   - URL: ${client.handshake.url}`);
    console.log(
      `   - 协议: ${client.handshake.headers['sec-websocket-version'] || 'HTTP'}`,
    );

    console.log('\n3. Headers:');
    Object.entries(client.handshake.headers).forEach(([key, value]) => {
      console.log(`   - ${key}: ${value as string}`);
    });

    console.log('\n4. Query 参数:');
    Object.entries(client.handshake.query).forEach(([key, value]) => {
      console.log(`   - ${key}: ${value as string}`);
    });

    console.log('\n5. Auth 数据:');
    console.log(`   - ${JSON.stringify(client.handshake.auth, null, 2)}`);

    console.log('\n6. 房间信息:');
    // 安全地处理 rooms
    const roomsList = this.getClientRooms(client);
    console.log(`   - 房间数量: ${roomsList.length}`);
    console.log(`   - 房间列表: ${roomsList.join(', ') || ''}`);

    // 额外显示房间详细信息
    if (roomsList.length > 0) {
      console.log('   - 详细房间信息:');
      roomsList.forEach((room, index) => {
        console.log(`     ${index + 1}. ${room}`);
      });
    }

    console.log('\n7. 数据存储:');
    console.log(`   - Data: ${JSON.stringify(client.data, null, 2)}`);

    console.log('=====================================\n');
  }
}

src/app.module.ts

第三步:注册

js
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ChatGateway } from './chat/chat.gateway';

@Module({
  imports: [],
  controllers: [AppController],
  providers: [AppService, ChatGateway],
})
export class AppModule {}

第四步:测试

shell
npm run start:dev